﻿#if UNITY_EDITOR && DEBUG_BEHAVIOUR_TREE
#define DEBUG_TREE
#endif
using GameToolkit.BehaviourTree;
using System;
using UnityEditor.Experimental.GraphView;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Devil;
using System.Collections.Generic;
using System.Reflection;

namespace GameToolkit.Editor
{
    using CompositeInfo = BehaviourTreeLib.CompositeInfo;

    //public partial class BehaviourTreeGraph
    //{

    internal class ExecutableNodeVisual : TreeGraphNode
#if DEBUG_TREE
        , IRuntimeBinding
#endif
    {
        internal readonly static string[] STAT_CLASS = { null, "stat-running", "stat-success", "stat-failed", "stat-success-pass", "stat-failed-pass" };
        internal readonly static Color[] STAT_COLOR = { Color.gray,
                new Color(0.1f, 0.4f, 0.9f),
                new Color(0.1f, 0.9f, 0.1f), new Color(0.9f, 0.2f, 0.1f),
                new Color(0.38f, 0.56f, 0.38f), new Color(0.5f, 0.34f, 0.34f)};

        protected ExecutableEntry mRuntimeInst;
        internal ExecutableEntry RuntimeInstance => mRuntimeInst;

        VisualElement mDecoratorContainer;
        internal VisualElement decoratorContainer => mDecoratorContainer;

        NodeState mState;
        internal NodeState State => mState;

        internal readonly Port startPort;
        //internal readonly Port conditionPort;
        internal readonly Port actionPort;

        public override Port GetInputPort(int portId)
        {
            if (portId == 0)
                return startPort;
            if (portId > 0 && portId <= decoratorContainer.childCount)
            {
                var deco = decoratorContainer[portId - 1] as DecoratorTitle;
                return deco == null ? null : deco.conditionPort;
            }
            return null;
        }

        public override int GetInputPortId(Edge edge)
        {
            var port = edge.input;
            if (port == startPort)
                return 0;
            for (int i = decoratorContainer.childCount - 1; i >= 0; i--)
            {
                var deco = decoratorContainer[i] as DecoratorTitle;
                if (deco != null && deco.conditionPort == port)
                    return i + 1;
            }
            return -1;
        }

        public override int GetOutputPortId(Edge edge)
        {
            return 0;
        }

        public override Port GetOutputPort(int id)
        {
            return actionPort;
        }

        public ExecutableNodeVisual(CompositeInfo info, Composite asset) : base(info, asset)
        {
            mState = NodeState.Inactive;

            startPort = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(Composite));
            startPort.portName = "START";
            startPort.portColor = STAT_COLOR[(int)mState];
            inputContainer.Add(startPort);

            //conditionPort = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(NodeState));
            //conditionPort.portName = "Condition?";
            //conditionPort.portColor = new Color(0.4f, 0.4f, 0.1f);
            //btContainer.Add(conditionPort);


            if (info.IsSubTreeRoot)
            {
                actionPort = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(Composite));
                actionPort.portName = "ACTION(s)";
                outputContainer.Add(actionPort);
                actionPort.portColor = STAT_COLOR[(int)mState];
            }
            else
            {
                actionPort = null;
            }

            mDecoratorContainer = new VisualElement();
            mDecoratorContainer.name = "decorator-container";
            btContainer.Add(mDecoratorContainer);

            var node = composite as ExecutableComposite;
            var decors = node.m_Decorations == null ? 0 : node.m_Decorations.Length;
            mDecoratorContainer.visible = decors > 0;
            for (int i = 0; i < decors; i++)
            {
                var decor = node.m_Decorations[i];
                if (decor == null)
                    continue;
                if (string.IsNullOrEmpty(decor._guid))
                    decor._guid = GUID.Generate().ToString();
                var lab = new DecoratorTitle(this);
                lab.composite = decor;
                mDecoratorContainer.Add(lab);
            }
        }

#if DEBUG_TREE
        public void BindRuntime(BehaviourTreePlayer player)
        {
            var inst = composite == null ? null : player.GetInstanceByGuid(composite.guid) as ExecutableEntry;
            if (inst != mRuntimeInst)
            {
                var wasRegist = mRuntimeInst is IBehaviourTreeBinder;
                if (mRuntimeInst != null)
                {
                    mRuntimeInst.OnStateChanged -= UpdateRuntimeState;
                }
                mRuntimeInst = inst;
                var isRegist = inst is IBehaviourTreeBinder;
                if (mRuntimeInst != null)
                {
                    mRuntimeInst.OnStateChanged += UpdateRuntimeState;
                }
                UpdateRuntimeState();
                if (wasRegist != isRegist)
                {
                    if (isRegist)
                    {
                        RegisterCallback<ClickEvent>(OnClickAction);
                        this.AddToClassList("subPlayer");
                    }
                    else
                    {
                        UnregisterCallback<ClickEvent>(OnClickAction);
                        this.RemoveFromClassList("subPlayer");
                    }
                }
            }
            for (int i = 0; i < mDecoratorContainer.childCount; i++)
            {
                if (mDecoratorContainer[i] is IRuntimeBinding bind)
                    bind.BindRuntime(player);
            }
        }

        public void UnbindRuntime()
        {
            for (int i = 0; i < mDecoratorContainer.childCount; i++)
            {
                if (mDecoratorContainer[i] is IRuntimeBinding bind)
                    bind.UnbindRuntime();
            }
            if (mRuntimeInst != null)
            {
                mRuntimeInst.OnStateChanged -= UpdateRuntimeState;
                mRuntimeInst = null;
                UnregisterCallback<ClickEvent>(OnClickAction);
                this.RemoveFromClassList("subPlayer");
            }
            UpdateRuntimeState();
        }
        
        void UpdateRuntimeState()
        {
            var stat = mRuntimeInst == null ? NodeState.Inactive : mRuntimeInst.State;
            if ((stat == NodeState.Success || stat == NodeState.Failed) && mRuntimeInst.IsStateObsolute)
                stat += 2;
            if (stat != mState)
            {
                var cname = STAT_CLASS[(int)mState];
                if (!string.IsNullOrEmpty(cname))
                    titleContainer.RemoveFromClassList(cname);
                mState = stat;
                cname = STAT_CLASS[(int)mState];
                if (!string.IsNullOrEmpty(cname))
                    titleContainer.AddToClassList(cname);
                startPort.portColor = STAT_COLOR[(int)mState];
                if (actionPort != null)
                {
                    actionPort.portColor = STAT_COLOR[(int)mState];
                }
            }
        }
        
        private void OnClickAction(ClickEvent evt)
        {
            if (evt.clickCount == 2 && mRuntimeInst is IBehaviourTreeBinder play)
            {
                var graph = GetFirstAncestorOfType<BehaviourTreeGraph>();
                var win = graph.AttachedWindow as BehaviourTreeEditor;
                if (win == null || play.behaviourTreeAsset == null)
                    return;
                win.DebugAsset(play.behaviourTreeAsset);
                evt.StopPropagation();
            }
        }

#endif

        bool ExecutableCompositeFilter(SearchTreeEntry node)
        {
            return node.userData is CompositeInfo cinfo && cinfo.IsExecutableNode;
        }

        bool LogicCompositeFilter(SearchTreeEntry node)
        {
            if (node.userData is CompositeInfo cinfo)
                return (cinfo.NodeType == BehaviourTreeLib.EBTNodeType.ComplexLogic || cinfo.NodeType == BehaviourTreeLib.EBTNodeType.Condition);
            else
                return node.userData is RegisterNodeVisual reg && reg.composite is RegisterComposite;
        }

        public override Port InstantiatePort(Orientation orientation, Direction direction, Port.Capacity capacity, Type type)
        {
            if (type == typeof(NodeState))
            {
                var port = new GraphPort<LinkEdge>(orientation, direction, capacity, type);
                port.AutoCreationFilter = LogicCompositeFilter;
                return port;
            }
            else
            {
                var port = new GraphPort<FlowEdge>(orientation, direction, capacity, type);
                port.AutoCreationFilter = ExecutableCompositeFilter;
                return port;
            }
        }

        internal void AddDecorator(Composite asset)
        {
            var node = composite as ExecutableComposite;
            var arr = new Composite[node.m_Decorations == null ? 1 : (node.m_Decorations.Length + 1)];
            if (arr.Length > 1)
                Array.Copy(node.m_Decorations, arr, node.m_Decorations.Length);
            arr[arr.Length - 1] = asset;
            node.m_Decorations = arr;
            if (string.IsNullOrEmpty(asset._guid))
                asset._guid = GUID.Generate().ToString();
            var lab = new DecoratorTitle(this);
            lab.composite = asset;
            mDecoratorContainer.Add(lab);
            mDecoratorContainer.visible = true;
        }

        internal override void NotifyRemoved()
        {
            base.NotifyRemoved();

            var decors = decoratorContainer.childCount;
            for (int i = 0; i < decors; i++)
            {
                var deco = decoratorContainer[i] as DecoratorTitle;
                if (deco != null)
                    deco.composite = null;
            }
        }

        public override void Validate()
        {
            base.Validate();
            var node = composite as ExecutableComposite;
            var decors = node == null || node.m_Decorations == null ? 0 : node.m_Decorations.Length;
            var size = mDecoratorContainer.childCount;
            while (size < decors)
            {
                var lab = new DecoratorTitle(this);
                mDecoratorContainer.Add(lab);
                size++;
            }
            for (int i = 0; i < decors; i++)
            {
                var item = node.m_Decorations[i];
                var oldIndex = -1;
                for (int k = i; k < size; k++)
                {
                    if (((DecoratorTitle)mDecoratorContainer[k]).composite == item)
                    {
                        oldIndex = k;
                        break;
                    }
                }
                if (oldIndex != i && oldIndex != -1)
                {
                    if (i > 0)
                        mDecoratorContainer[oldIndex].PlaceInFront(mDecoratorContainer[i - 1]);
                    else
                        mDecoratorContainer[oldIndex].PlaceBehind(mDecoratorContainer[0]);
                }
                ((DecoratorTitle)mDecoratorContainer[i]).composite = item;
                //text = BehaviourTreeLib.Instance.GetDescription(item);
            }
            while (size > decors)
            {
                var index = --size;
                ((DecoratorTitle)mDecoratorContainer[index]).composite = null;
                mDecoratorContainer.RemoveAt(index);
            }
        }

        public override void SaveCompositeConnections(MinHeap<INodeWithComposite> nodeBuffer)
        {
            var exec = composite as ExecutableComposite;
            if (exec == null)
                return;
            if (compositeInfo.IsSubTreeRoot)
            {
                // get children
                nodeBuffer.Clear();
                foreach (var edge in actionPort.connections)
                {
                    var child = edge.input.node as ExecutableNodeVisual;
                    if (child == null || child.composite == null)
                        continue;
                    nodeBuffer.Add(child);
                }
                exec.m_Children = BehaviourTreeGraphUtils.ToComposites(nodeBuffer);
            }
            else
            {
                exec.m_Children = new Composite[0];
            }
            // get condition reference
            for (int i = 0; i < decoratorContainer.childCount; i++)
            {
                var deco = decoratorContainer[i] as DecoratorTitle;
                var conditionRef = deco == null ? null : deco.composite as ConditionReference;
                if (conditionRef == null)
                    continue;
                foreach (var edge in deco.conditionPort.connections)
                {
                    var cond = edge.output.node as INodeWithComposite;
                    if (cond != null && cond.composite != null)
                    {
                        CompositeState cinput;
                        cinput.composite = cond.composite;
                        cinput.state = (NodeState)cond.GetOutputPortId(edge);
                        conditionRef.m_Input = cinput;
                        break;
                    }
                }
            }
        }

        public override void LoadCompositeConnections(BehaviourTreeGraph.GraphEdgeLoader edgeLoader)
        {
            if (composite is ExecutableComposite exec)
            {
                var children = exec.m_Children;
                var len = children == null ? 0 : children.Length;
                for (int i = 0; i < len; i++)
                {
                    var child = edgeLoader.GetNode(children[i]);
                    if (child != null)
                    {
                        var port = child.GetInputPort(0);
                        edgeLoader.NewEdge<FlowEdge>(port, actionPort);
                    }
                }
                len = decoratorContainer.childCount;
                for (int i = 0; i < len; i++)
                {
                    var cond = decoratorContainer[i] as DecoratorTitle;
                    if (cond == null || cond.composite is not ConditionReference)
                        continue;
                    var cinput = ((ConditionReference)cond.composite).m_Input;
                    var node = edgeLoader.GetNode(cinput.composite);
                    if (node != null)
                    {
                        var port = node.GetOutputPort((int)cinput.state);
                        edgeLoader.NewEdge<LinkEdge>(cond.conditionPort, port);
                    }
                }
            }
        }

        public override bool IsCompatiblePort(Port port)
        {
            return true;
        }

        public override void AutoConnectOnCreate(BehaviourTreeGraph graph, Port port)
        {
            if(port.node is ExecutableNodeVisual exec)
            {
                if(port.direction == Direction.Input)
                {
                    graph.DeleteElements(port.connections);
                    port.DisconnectAll();
                    graph.NewEdge<FlowEdge>(port, actionPort);
                }
                else
                {
                    graph.NewEdge<FlowEdge>(startPort, port);
                }
            }
        }

        // target, runtimeInst, cmenu, method, validate
        readonly int cid_composite = 0;
        readonly int cid_runtime = 1;
        readonly int cid_name = 2;
        readonly int cid_method = 3;
        readonly int cid_validte = 4;

        void GetContextMenuInfo(Composite compositeInst, BehaviourTreeObject runtimeInst, string prefix, Dictionary<string, object[]> menus)
        {
            var flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.InvokeMethod;
            var tp = compositeInst.GetType();
            var baseType = typeof(Composite);
            while (tp != null && tp != baseType)
            {
                var methods = tp.GetMethods(flags);
                tp = tp.BaseType;
                foreach (var method in methods)
                {
                    var menuItems = method.GetCustomAttributes(true);
                    var len = menuItems == null ? 0 : menuItems.Length;
                    for (int i = 0; i < menuItems.Length; i++)
                    {
                        if (menuItems[i] is ContextMenu cmenu)
                        {
                            object[] menu;
                            if (!menus.TryGetValue(cmenu.menuItem, out menu))
                            {
                                menu = new object[5];
                                menus.Add(cmenu.menuItem, menu);
                                menu[cid_composite] = compositeInst;
                                menu[cid_runtime] = runtimeInst;
                                if (string.IsNullOrEmpty(prefix))
                                    menu[cid_name] = cmenu.menuItem;
                                else
                                    menu[cid_name] = $"{prefix}/{cmenu.menuItem}";
                            }
                            menu[cmenu.validate ? cid_validte : cid_method] = method;
                           
                        }
                    }
                }
            }
        }

        public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
        {
            evt.menu.AppendAction("Refresh", (dt) => Validate(), DropdownMenuAction.Status.Normal);

            Dictionary<string, object[]> menus = new Dictionary<string, object[]>();
            GetContextMenuInfo(composite, mRuntimeInst, "", menus);
            for(int i=0;i< mDecoratorContainer.childCount;i++)
            {
                var decor = mDecoratorContainer[i] as DecoratorTitle;
                if (decor == null)
                    continue;
                GetContextMenuInfo(decor.composite, decor.runtimeInstance, BehaviourTreeLib.Instance.GetTitle(decor.composite, true), menus);
            }
            foreach (var kv in menus)
            {
                var menu = kv.Value;
                evt.menu.AppendAction(menu[cid_name].ToString(), ExecuteContextMenu, GetContextMenuState, menu);
            }
            evt.menu.AppendSeparator();
            if (mRuntimeInst != null || EditorApplication.isPlayingOrWillChangePlaymode)
                return;
            base.BuildContextualMenu(evt);
        }

        bool InvokeContextMenuValidater(MethodInfo method, object target, object runtimeInstance)
        {
            try
            {
                var arg = method.GetParameters();
                if (arg == null || arg.Length == 0)
                    return (bool)method.Invoke(target, new object[0]);
                else
                    return (bool)method.Invoke(target, new object[] { runtimeInstance });
            }
            catch (System.Exception e)
            {
                Debug.LogError($"\"{method.DeclaringType.Name}.{method.Name}\" is not correct as ContextMenu validater.\n\tdefination: bool MethodName() or bool MethodName(runtimeType runtimeInst)\n\n{e}");
                return false;
            }
        }

        private DropdownMenuAction.Status GetContextMenuState(DropdownMenuAction action)
        {
            bool valid = true;
            var menu = action.userData as object[];
            if (menu[4] is MethodInfo method)
            {
                valid = InvokeContextMenuValidater(method, menu[cid_composite], menu[cid_runtime]);
            }
            return valid ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled;
        }

        private void ExecuteContextMenu(DropdownMenuAction action)
        {
            var menu = action.userData as object[];
            if (menu[cid_method] is MethodInfo method)
            {
                try
                {
                    var arg = method.GetParameters();
                    if (arg == null || arg.Length == 0)
                        method.Invoke(menu[cid_composite], new object[0]);
                    else
                        method.Invoke(menu[cid_composite], new object[] { menu[cid_runtime] });
                }
                catch (System.Exception e)
                {
                    Debug.LogError($"\"{method.DeclaringType.Name}.{method.Name}\" is not correct as ContextMenu.\n\tdefination: void MethodName() or void MethodName(runtimeType runtimeInst)\n\n{e}");
                }
            }
        }

    }
}
